package fr.inria.arles.webService.androidClientGenerator;

import japa.parser.JavaParser;
import japa.parser.ParseException;
import japa.parser.ast.CompilationUnit;
import japa.parser.ast.ImportDeclaration;
import japa.parser.ast.body.ClassOrInterfaceDeclaration;
import japa.parser.ast.body.FieldDeclaration;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.Parameter;
import japa.parser.ast.expr.AnnotationExpr;
import japa.parser.ast.expr.NameExpr;
import japa.parser.ast.visitor.VoidVisitorAdapter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Scanner;
import java.util.regex.Pattern;
/**
 *
 * @author ajay
 */
public class AndroidClientGenerator {
        
    // Service and Port Class name suffixes for clients generated for
    // external services
    public static final String CLIENT_SERVICE_CLASS_SUFFIX = "Service";
    public static final String CLIENT_PORT_CLASS_SUFFIX = "Port";    

    public static void generateAndroidClient(String path, String wsdlUrl, String serviceName,String namespace,
            String portName,String targetPackage) throws IOException, ParseException {
        
        File dir = new File(path);
        if (!dir.isDirectory()) {
            throw new IOException("Invalid path: " + path);
        }
        
        // Delete .class files generated
        FileFilter classFilter = new FileFilter() {

            @Override
            public boolean accept(File pathname) {
                return pathname.getName().endsWith(".class");
            }
        };
        File[] classFiles = dir.listFiles(classFilter);
        int filesDel = 0;
        for (File classFile : classFiles) {
            if (classFile.delete() == true) {
                filesDel++;
            }
        }
        System.out.println("Deleted " + filesDel + " .class files out of "
                + classFiles.length);
        
        String serviceClass=serviceName + CLIENT_SERVICE_CLASS_SUFFIX;
        String portClass=portName + CLIENT_PORT_CLASS_SUFFIX;
        
        File objFactoryFile = new File(path+File.separator+"ObjectFactory.java");
        objFactoryFile.delete();
        File packageInfoFile = new File(path+File.separator+"package-info.java");
        packageInfoFile.delete();
                
        // Generate port implementation
        File portClassFile = new File(path+File.separator+portClass+".java");
        List<OperationData> operationList = getOperationDataList(portClassFile);
        PortImplGenerator portImplGen = new PortImplGenerator();
        portImplGen.generate(path, targetPackage, serviceName, portClass, operationList);
        System.out.println("Generated Port Impl file: " + portClass + "Impl");
        
        // Remove annotations and imports from all java sources generated by wsimport
        FileFilter javaSourceFilter = new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.getName().endsWith(".java");
            }
        };
        File[] files = dir.listFiles(javaSourceFilter);        
        for (File file : files) {
            String fileName=file.getName();
            if(     !fileName.equals(portClass+"Impl.java") &&                    
                    !fileName.equals("Constants.java") )
                removeClassAnnotationsAndImports(file);
        }
        
        // Generate constants file
        System.out.println("Generated Constants file: Constants.java");
        ConstantsGenerator conGer = new ConstantsGenerator();
        conGer.generate(path, targetPackage, wsdlUrl, namespace);

        // Generate service file        
        File serviceClassFile = new File(path+File.separator+serviceClass+".java");
        serviceClassFile.delete();
        ServiceGenerator serGen = new ServiceGenerator();
        serGen.generate(path, targetPackage, serviceClass, portClass,
                portName);
        System.out.println("Generated Service file: " + serviceClass + ".java");
        
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args)
            throws IOException, ParseException {
        Process pro;
        Scanner s;
        Properties prop = System.getProperties();
        
        if(loadProperties(prop, "wsdl.properties"))
        {
            System.out.println("Properties loaded from wsdl.properties file");           
        }
        
        String path = prop.getProperty("PATH");
        String pack = prop.getProperty("TARGET_PACKAGE");
        String url = prop.getProperty("WSDL_URL");
        String portName = prop.getProperty("PORT_NAME");
        String relPath = pack.replaceAll(Pattern.quote("."), "/") + "/";

        ProcessBuilder pb = new ProcessBuilder("wsimport","-s",path,"-d",path,
                   "-keep","-p",pack,"-b","bind.xml", "-target", "2.0", url);                              
        pb.redirectErrorStream(true);
        pro = pb.start();
        s = new Scanner(pro.getInputStream());
        while (s.hasNextLine()) {
            System.out.println("wsimport:\t" + s.nextLine());
        }
        String location = path + "/" + relPath;
        generateAndroidClient(
                location,
                url,
                prop.getProperty("SERVICE_NAME"),
                prop.getProperty("NAMESPACE"),                
                portName,
                pack);

        boolean success = true;;
        if (!AllowedTypes.complexInput.isEmpty()) {
            System.out.println();
            System.out.println("Following complex input types not supported:");
            for (String x : AllowedTypes.complexInput) {
                System.out.println(x);
            }
            success = false;
        }

        if (!AllowedTypes.complexOutput.isEmpty()) {
            System.out.println();
            System.out.println("Following complex output types not supported:");
            for (String x : AllowedTypes.complexOutput) {
                System.out.println(x);
            }
            success = false;
        }


        if (!success) {
            System.out.println();
            System.out.println("*** ERROR ***");
            System.out.println("Client Generation FAILED due to presence of complex types");
            System.out.println("Incomplete sources generated at:" + path);
            System.out.println("Please modify generated sources to add support for complex types");
            System.out.println("DON'T FORGET TO INCLUDE "
                    + "lib/ksoap2-android-assembly-2.4-jar-with-dependencies.jar"
                    + " library in your android project");
        
        } else {
            System.out.println();
            System.out.println("*** SUCCESS ***");
            System.out.println("Client Generation succeeded");
            System.out.println("Sources generated at:" + path);
            System.out.println("Compiling and generating jar file");

            pb = new ProcessBuilder("javac", "-cp", "lib/ksoap2-android-assembly-2.4-jar-with-dependencies.jar", "-d", path, location + "*.java");
            pb.redirectErrorStream(true);
            pro = pb.start();
            s = new Scanner(pro.getInputStream());
            while (s.hasNextLine()) {
                System.out.println("javac:\t" + s.nextLine());
            }

            pb = new ProcessBuilder("jar", "-cvf", portName + "Client.jar", relPath + "*.class");
            pb.directory(new File(path));
            pb.redirectErrorStream(true);
            pro = pb.start();
            s = new Scanner(pro.getInputStream());
            while (s.hasNextLine()) {
                System.out.println("jar:\t" + s.nextLine());
            }
            System.out.println();                       
            System.out.println("*** PROCESS COMPLETED ***");
            System.out.println("Sources generated at:" + path);
            System.out.println("jar generated at:" + path + portName + "Client.jar");
            System.out.println("DON'T FORGET TO INCLUDE "
                    + "lib/ksoap2-android-assembly-2.4-jar-with-dependencies.jar"
                    + " library in your android project");
        }


    }

    private static List<OperationData> getOperationDataList(File pFile) throws IOException {
        try {
            List<OperationData> list = new ArrayList<OperationData>();
            FileInputStream portStream = new FileInputStream(pFile);
            CompilationUnit portCu = JavaParser.parse(portStream);
            portStream.close();
            new PortMethodVisitor().visit(portCu, list);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
            throw new IOException("Error parsing Java file:" + pFile.getName());            
        }

    }

    /**
     * Simple visitor implementation for visiting MethodDeclaration nodes. 
     */
    private static class PortMethodVisitor extends VoidVisitorAdapter {

        @Override
        public void visit(MethodDeclaration md, Object arg) {
            String outputType = md.getType().toString();
            if (!AllowedTypes.returnVal.containsKey(outputType)) {
                System.out.println("Complex Type: "
                        + outputType + " not supported as output type");
                AllowedTypes.complexOutput.add(outputType);
                return;
            }
            List<OperationData> list = (List<OperationData>) arg;
            List<AnnotationExpr> annotations = md.getAnnotations();
            String operationName = md.getName();
            String[] s;
            for (AnnotationExpr an : annotations) {
                if ((s = AnnotationParser.parse(an.toString(), "WebMethod", "operationName")) != null) {
                    if (s[0] != null) {
                        operationName = s[0];
                    }
                }
            }
            md.setAnnotations(new ArrayList<AnnotationExpr>());
            List<Parameter> params = md.getParameters();
            String input[][] = new String[params.size()][3];
            for (int i = 0; i < params.size(); i++) {
                input[i][0] = params.get(i).getId().getName();
                String inputType;
                input[i][1] = inputType = params.get(i).getType().toString();
                if (!AllowedTypes.soapClass.containsKey(inputType)) {
                    System.out.println("Complex Type: "
                            + inputType + " not supported as input type");
                    AllowedTypes.complexInput.add(inputType);
                    return;
                }
                input[i][2] = input[i][0];
                List<AnnotationExpr> pAnnotations = params.get(i).getAnnotations();
                for (AnnotationExpr an : pAnnotations) {
                    if ((s = AnnotationParser.parse(an.toString(), "WebParam", "name")) != null) {
                        if (s[0] != null) {
                            input[i][2] = s[0];
                        }
                    }
                }

            }

            List<NameExpr> excepts = md.getThrows();
            String[] exceptions = new String[excepts.size()];
            for (int i = 0; i < excepts.size(); i++) {
                exceptions[i] = excepts.get(i).getName();
            }
            list.add(new OperationData(md.getName(), operationName, input,
                    outputType, exceptions));

        }
    }

    /**
     * Simple visitor implementation for visiting ClassDeclaration nodes. 
     */
    private static class ClassAnnotationRemoveVisitor extends VoidVisitorAdapter {

        @Override
        public void visit(ClassOrInterfaceDeclaration n, Object arg) {
            n.setAnnotations(new ArrayList<AnnotationExpr>());

        }
    }
    
    /**
     * Simple visitor implementation for visiting ClassDeclaration nodes. 
     */
    private static class ServiceClassAnnotationeVisitor extends VoidVisitorAdapter {

        @Override
        public void visit(ClassOrInterfaceDeclaration cd, Object arg) {            
            List<AnnotationExpr> annotations = cd.getAnnotations();
            String[] s;
            for (AnnotationExpr an : annotations) {
                if ((s = AnnotationParser.parse(an.toString(), "WebServiceClient", "targetNamespace")) != null) {
                    if (s[0] != null) {
                       arg=s[0];
                    }
                }
            }

        }
    }

    /**
     * Simple visitor implementation for visiting MethodDeclaration nodes. 
     */
    private static class MethodAnnotationRemoveVisitor extends VoidVisitorAdapter {

        @Override
        public void visit(MethodDeclaration n, Object arg) {
            n.setAnnotations(new ArrayList<AnnotationExpr>());

            List<Parameter> params = n.getParameters();
            if (params != null) {
                for (Parameter p : params) {
                    p.setAnnotations(new ArrayList<AnnotationExpr>());
                }
            }            
        }
    }
    
    /**
     * Simple visitor implementation for visiting FieldDeclaration nodes. 
     */
    private static class FieldAnnotationRemoveVisitor extends VoidVisitorAdapter {

        @Override
        public void visit(FieldDeclaration n, Object arg) {
            n.setAnnotations(new ArrayList<AnnotationExpr>());                        
        }
    }

    private static String getServiceNameSpace(File serviceFile) throws FileNotFoundException, ParseException, IOException
    {
        FileInputStream in = new FileInputStream(serviceFile);
        CompilationUnit cu;
        cu = JavaParser.parse(in);
        String serviceNamespace=null;
        new ServiceClassAnnotationeVisitor().visit(cu,serviceNamespace);        
        in.close();       
        return  serviceNamespace;
    }
    
    private static void removeClassAnnotationsAndImports(File file) throws IOException, ParseException {
        FileInputStream in = new FileInputStream(file);
        CompilationUnit cu;
        cu = JavaParser.parse(in);
        cu.setImports(new ArrayList<ImportDeclaration>());
        in.close();

        new ClassAnnotationRemoveVisitor().visit(cu, null);
        new MethodAnnotationRemoveVisitor().visit(cu, null);
        new FieldAnnotationRemoveVisitor().visit(cu, null);

        FileWriter out = new FileWriter(file);
        out.append(cu.toString());
        out.close();
        System.out.println("Annotations removed from file:" + file.getName());
    }

    public static boolean loadProperties(Properties properties, String filename) {
        try {
            properties.load(new FileInputStream(filename));
            System.out.println("Propeties loaded from file:" + filename);
            return true;
        } catch (Exception e) {
            // Just return false
        }
        return false;
    }
}